Skip to content

feat: add useAttribute hook for self-contained property-attribute reflection#97

Draft
cristinecula wants to merge 1 commit into
mainfrom
feature/neo-1055-add-useattribute-hook-to-pion-for-property-attribute
Draft

feat: add useAttribute hook for self-contained property-attribute reflection#97
cristinecula wants to merge 1 commit into
mainfrom
feature/neo-1055-add-useattribute-hook-to-pion-for-property-attribute

Conversation

@cristinecula

@cristinecula cristinecula commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Note: This is one of three alternative implementations for NEO-1055. See also:

Summary

  • Adds a standalone useAttribute hook that provides bidirectional property ↔ attribute sync with built-in type coercion, loop prevention, and MutationObserver-based attribute observation
  • Unlike useProperty with reflect, useAttribute is fully self-contained — no need to declare observedAttributes separately
  • Supports Boolean, String, and Number types with automatic coercion and optional custom defaults

API

const [opened, setOpened] = useAttribute('opened', Boolean);           // default: false
const [name, setName] = useAttribute('name', String, 'unnamed');       // custom default
const [count, setCount] = useAttribute('count', Number);               // default: 0

Signature: useAttribute<T>(name: string, type: AttributeType, defaultValue?: T): StateTuple<T>

How it works

  • Uses a shared MutationObserver per host element (stored via symbol) to observe attribute changes — no observedAttributes declaration needed
  • Loop prevention via reflectingSymbol: when the hook reflects a property to an attribute, it sets a flag so the MutationObserver callback skips self-caused changes
  • Type coercion table:
Type Attribute → JS JS → Attribute Default
Boolean present → true, absent → false true"", false → remove false
String attribute value as-is setAttribute(value) ""
Number parseFloat(value) String(value) 0

Features

  • Property → attribute reflection (automatic)
  • Attribute → property sync (via MutationObserver)
  • *-changed event dispatch (cancelable, same convention as useProperty)
  • preventDefault() blocks both property and attribute update
  • Function updater pattern: setCount(prev => prev + 1)
  • Kebab-case attribute ↔ camelCase property conversion
  • Compatible with lift() pattern for parent-controlled state
  • Honors initial HTML attributes (<el opened>)
  • Respects parent .prop= bindings
  • Shared MutationObserver across multiple useAttribute calls on same host
  • Proper teardown on disconnect

Test coverage

22 test cases covering:

  • Boolean, String, Number types with reflection
  • Initial attribute values and custom defaults
  • Function updater pattern
  • Feedback loop prevention
  • External setAttribute/removeAttribute via MutationObserver
  • Event dispatch and preventDefault
  • Kebab-case ↔ camelCase conversion
  • Lift pattern with parent/child
  • Multiple useAttribute hooks on same host
  • No observedAttributes required
  • Parent .prop= override

All 91 tests pass (existing + new).

Files changed

File Change
src/use-attribute.ts New hook implementation (~280 lines)
src/symbols.ts Added reflectingSymbol and attributeObserverSymbol
src/core.ts Added useAttribute export
test/use-attribute.test.ts Comprehensive test suite (~530 lines)

Closes NEO-1055

@changeset-bot

changeset-bot Bot commented Feb 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7dd8558

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@netlify

netlify Bot commented Feb 16, 2026

Copy link
Copy Markdown

Deploy Preview for pionjs ready!

Name Link
🔨 Latest commit 7dd8558
🔍 Latest deploy log https://app.netlify.com/projects/pionjs/deploys/6992be4a71c8ff00089e299b
😎 Deploy Preview https://deploy-preview-97--pionjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

…lection

Add a standalone useAttribute hook that provides bidirectional
property <-> attribute sync with built-in loop prevention, type
coercion, and MutationObserver-based attribute observation.

Unlike useProperty with reflect, useAttribute is fully self-contained:
no need to declare observedAttributes separately. It supports Boolean,
String, and Number types with automatic coercion.

API: const [value, setValue] = useAttribute(name, type, defaultValue?)

Closes NEO-1055
@cristinecula cristinecula force-pushed the feature/neo-1055-add-useattribute-hook-to-pion-for-property-attribute branch from 7004a53 to 7dd8558 Compare February 16, 2026 06:50
@cristinecula cristinecula changed the title feat: add reflect option to useProperty for property-attribute reflection feat: add useAttribute hook for self-contained property-attribute reflection Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant